package com.crazypie.soundgraph 
{
	import com.crazypie.soundgraph.ISoundNode;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.utils.getDefinitionByName;
	/**
	 * ...
	 * @author Paul Pepper
	 */
	public class SoundNode extends EventDispatcher implements ISoundNode
	{
		//--------------------------------------
		// CLASS CONSTANTS
		//--------------------------------------
		public static const MAX_LOOPS:int = 10000000;
		
		//--------------------------------------
		//  PRIVATE VARIABLES
		//--------------------------------------
		//node properties
		private var m_children:Vector.<ISoundNode>
		private var m_parent:ISoundNode;
		
		//sound properties
		private var m_sound:Sound;
		private var m_channel:SoundChannel;
		private var m_loops:uint = 1;//total loops to play
		private var m_loopsRemaining:uint = 1;//the number of loops left to play
		private var m_isPlaying:Boolean = false;
		private var m_pause:Boolean = false;
		private var m_pauseChildren:Boolean = false;
		private var m_pausedByAncestor:Boolean = false;
		
		private var m_pausePosition:Number;
		private var m_loopDuration:Number = 0;
		
		private var m_endHandler:Function;
		
		//sound transforms
		private var m_transform:SoundTransform;
		private var m_parentTransform:SoundTransform;
		private var m_computedTransform:SoundTransform;
		private var m_mute:Boolean = false;
		
		public function SoundNode( pSound:Sound = null )
		{
			m_children = new Vector.<ISoundNode>;
			
			m_transform = new SoundTransform();
			m_parentTransform = null;
			m_computedTransform = new SoundTransform();
			
			sound = pSound;
		}
		
		//--------------------------------------
		//  PUBLIC METHODS
		//--------------------------------------
		public function addChild(child:ISoundNode):void
		{
			if ( child )
			{
				child.setParent( this );
				
				//add as child
				m_children[m_children.length] = child;
				
				
			}
		}
		
		public function removeChild(child:ISoundNode):ISoundNode
		{
			var position:int = m_children.indexOf( child );
			
			if ( position > -1 )
			{
				if ( child.parent )
				{
					child.setParent( null );
				}
				
				m_children.splice( position, 1 );//remove child at supplied position
				
				return child;//return child
			}
			
			//supplied node not a child of this node
			return null;
		}
		
		public function removeChildAt(position:uint):ISoundNode
		{
			if ( m_children.length > position )
			{
				var child:ISoundNode = m_children[position];
				
				if ( child.parent )
				{
					child.setParent( null );
				}
				
				m_children.splice( position, 1 );//remove child at supplied position
				
				return child;//return child
			}
			
			return null;
		}
		
		public function getChildAt( childIndex:uint ):ISoundNode
		{
			return m_children[childIndex];
		}
		
		public function setParent( newParent:ISoundNode ):void
		{
			if ( m_parent )
			{
				var p:ISoundNode = m_parent;
				
				m_parent = null;
				
				p.removeChild( this );
				
				parentTransform = null;
				
				p.removeEventListener( PauseChildrenEvent.PAUSE_CHILDREN, onParentPauseChildren );
				
				//If was paused by ancestor, unpause
				if ( m_pausedByAncestor )
				{
					onParentPauseChildren( new PauseChildrenEvent( false ) );
				}
			}
			
			if ( newParent )
			{
				m_parent = newParent;
				
				newParent.addEventListener( PauseChildrenEvent.PAUSE_CHILDREN, onParentPauseChildren, false, 0, true );
				
				//now check initial state of parent pause
				var ancestor:ISoundNode = this;
				
				while ( ancestor = ancestor.parent )
				{
					if ( ancestor.pauseChildren )
					{
						onParentPauseChildren( new PauseChildrenEvent( true ) )
					}
				}
			}
		}
		
		//sound methods
		
		public function play( startTime:Number = 0, loops:uint = 1 ):void
		{
			trace( 'SoundNode::play('+arguments+')' );
			if ( m_sound )
			{
				m_loops = m_loopsRemaining = loops;
				m_isPlaying = true
				
				if ( m_channel )
				{
					stop();
				}
				
				_play( startTime );
				
				m_endHandler = stop;
				
				dispatchEvent( new SoundNodeEvent( SoundNodeEvent.PLAY ) );
			}
		}
		
		public function playAndClear( startTime:Number = 0, loops:uint = 1 ):void
		{
			play( startTime, loops );
			
			m_endHandler = clear;
		}
		
		public function playAndDestroy( startTime:Number = 0, loops:uint = 1 ):void
		{
			play( startTime, loops );
			
			m_endHandler = destroy;
		}
		
		public function stop():void
		{
			trace( 'SoundNode::stop('+arguments+')' );
			m_isPlaying = false;
			m_pause = false;
			
			m_pausePosition = 0;
			m_loops = 1;
			m_loopsRemaining = 0;
			
			m_endHandler = null;
			
			if ( m_channel )
			{
				removeChannel();
			}
			
			dispatchEvent( new SoundNodeEvent( SoundNodeEvent.STOP ) );
		}
		
		public function stopChildren():void
		{
			for ( var i:int = m_children.length - 1; i > -1; --i )
			{
				m_children[i].stop();
				m_children[i].stopChildren();
			}
		}
		
		public function clear():void
		{
			trace( 'SoundNode::clear('+arguments+')' );
			stop();
			
			if( m_sound )
			{
				m_sound = null;
				m_loopDuration = 0;
			}
			
			if( m_channel )
			{
				removeChannel();
			}
		}
		
		public function destroy():void
		{
			trace( 'SoundNode::destroy('+arguments+')' );
			for ( var i:int = m_children.length - 1; i > -1; --i )		//destroy all children
			{
				m_children[i].destroy();
			}
			
			m_children.length = 0;//remove all ref's the children
			
			clear();													//remove all sound info
			
			setParent( null );											//remove from parent
			
			m_transform = null;											//clear all refs for garbage collection
			m_parentTransform = null;
			m_computedTransform = null;
		}
		
		public function changeVolume( changeBy:Number ):void
		{
			volume += changeBy;
		}
		
		//--------------------------------------
		//  EVENT HANDLERS
		//--------------------------------------
		
		private function onParentPauseChildren(e:PauseChildrenEvent):void 
		{
			trace( 'SoundNode::onParentPauseChildren('+arguments+')' );
			
			var curPause:Boolean = _pause;//record if this is currently paused
			
			m_pausedByAncestor = e.isPaused;//record ancestor muting status
			
			_doPause( curPause );//see how this affects the actual playing status of this sound
			
			//tell children, unless this node wants them paused
			if ( !m_pauseChildren )
			{
				dispatchEvent( new PauseChildrenEvent( m_pausedByAncestor ) );
			}
		}
		
		//--------------------------------------
		//  GETTERS/SETTERS
		//--------------------------------------
		
		public function get mute():Boolean
		{
			return m_mute;
		}
		
		public function set mute( newMute:Boolean ):void
		{
			if ( newMute != mute )
			{
				m_mute = newMute;
				
				updatedComputedTransform();
				
				dispatchEvent( new SoundNodeEvent( SoundNodeEvent.MUTE ) );
			}
		}
		
		public function get numChildren():uint
		{
			return m_children.length;
		}
		
		public function get parent():ISoundNode
		{
			return m_parent;
		}
		
		public function get sound():Sound
		{
			return m_sound;
		}
		
		public function set sound( newSound:Sound ):void
		{
			clear();//clear all sound values before setting new sound
			
			m_sound = newSound;
			
			if ( newSound != null )
			{
				m_loopDuration = m_sound.length;
			}
			else
			{
				m_loopDuration = 0;
			}
		}
		
		
		public function get transform():SoundTransform
		{
			return m_transform;
		}
		
		public function set transform( newTransform:SoundTransform ):void
		{
			m_transform = newTransform;
			
			updatedComputedTransform()
		}
		
		public function get computedTransform():SoundTransform
		{
			return m_computedTransform;
		}
		
		public function set parentTransform( parentTransform:SoundTransform ):void
		{
			m_parentTransform = parentTransform;
			
			updatedComputedTransform();
		}
		
		public function get leftToLeft():Number
		{
			return m_transform.leftToLeft;
		}
		
		public function set leftToLeft(newLeftToLeft:Number):void
		{
			if ( m_transform )
			{
				m_transform.leftToLeft = newLeftToLeft;
				
				updatedComputedTransform();
			}
		}
		
		public function get leftToRight():Number
		{
			return m_transform.leftToRight;
		}
		
		public function set leftToRight(newLeftToLeft:Number):void
		{
			if ( m_transform )
			{
				m_transform.leftToRight = newLeftToLeft;
				
				updatedComputedTransform();
			}
		}
		
		public function get rightToLeft():Number
		{
			return m_transform.rightToLeft;
		}
		
		public function set rightToLeft(newRightToLeft:Number):void
		{
			if ( m_transform )
			{
				m_transform.rightToLeft = newRightToLeft;
				
				updatedComputedTransform();
			}
		}
		
		public function get rightToRight():Number
		{
			return m_transform.rightToRight;
		}
		
		public function set rightToRight(newRightToRight:Number):void
		{
			if ( m_transform )
			{
				m_transform.rightToRight = newRightToRight;
				
				updatedComputedTransform();
			}
		}
		
		public function get pan():Number
		{
			return m_transform.pan;
		}
		
		public function set pan(newPan:Number):void
		{
			if ( m_transform )
			{
				m_transform.pan = newPan;
				
				updatedComputedTransform();
			}
		}
		
		public function get volume():Number
		{
			return m_transform.volume;
		}
		
		public function set volume( vol:Number ):void
		{
			if ( m_transform )
			{
				m_transform.volume = vol;
				
				updatedComputedTransform();
			}
		}
		
		//playhead status
		
		public function get isPlaying():Boolean
		{
			return m_isPlaying;
		}
		
		public function get pause():Boolean
		{
			return m_pause;
		}
		
		public function set pause( newPause:Boolean ):void
		{
			trace( 'SoundNode::pause(' + arguments + ')' );
			
			var curPause:Boolean = _pause;
			
			m_pause = newPause;//record that this has been unpaused
			
			_doPause( curPause );//see how this affects the actual playing status of this sound
			
			dispatchEvent( new SoundNodeEvent( SoundNodeEvent.PAUSE ) );
		}
		
		public function get pauseChildren():Boolean
		{
			return m_pauseChildren;
		}
		
		public function set pauseChildren( newPauseChildren:Boolean ):void
		{
			if ( newPauseChildren != m_pauseChildren )
			{
				m_pauseChildren = newPauseChildren;
				
				if ( newPauseChildren || !m_pausedByAncestor )
				{
					dispatchEvent( new PauseChildrenEvent( newPauseChildren ) );
				}
			}
		}
		
		/**
		 * Internal getter to determine what the pause status really is right now
		 */
		protected function get _pause():Boolean
		{
			trace( m_pause, m_pausedByAncestor )
			return m_pause || m_pausedByAncestor;
		}
		
		public function get position():Number
		{
			if ( m_isPlaying )
			{
				var loopPosition:Number = m_pause ? m_pausePosition : m_channel.position;
				
				if ( m_loops == 0 )
				{
					//if this is an infinitely looping sound return position within a loop
					return loopPosition;
				}
				else//return position within the loops
				{
					return loopPosition + ( m_loopDuration * ( m_loops - m_loopsRemaining ) );
				}
			}
			
			return 0;
		}
		
		public function set position( newPosition:Number ):void
		{
			if ( m_isPlaying )
			{
				//if seeking outside bounds of loop, stop
				if ( newPosition >= duration )
				{
					stop();
					
					return;
				}
				else if ( newPosition < 0 )
				{
					if ( m_loops == 0 )
					{
						newPosition = m_loopDuration + ( newPosition % m_loopDuration );
					}
					else
					{
						newPosition = 0;
					}
				}
				
				if ( m_loops != 0 )//if not set to infinite loop
				{
					//work out what loop number you're now in
					var loopNumber:int = Math.floor( newPosition / m_loopDuration );
					
					m_loopsRemaining = m_loops - loopNumber;
				}
				
				//change to position within a loop
				newPosition = newPosition % m_loopDuration;
				
				if ( m_pause )
				{
					m_pausePosition = newPosition;
				}
				else
				{
					//restart playing
					_play( newPosition );
				}
			}
		}
		
		public function get duration():Number
		{
			if ( m_sound )
			{
				if ( m_loops == 0 )
				{
					return Number.POSITIVE_INFINITY;
				}
				else
				{
					return m_loopDuration * m_loops;
				}
			}
			
			return 0;
		}
		
		public function get loopDuration():Number
		{
			if ( m_sound )
			{
				return m_loopDuration;
			}
			
			return 0;
		}
		
		public function get remainingTime():Number
		{
			if ( m_isPlaying )
			{
				if ( m_sound )
				{
					return remainingLoops * m_loopDuration;
				}
			}
			
			return 0;
		}
		
		public function get remainingLoops():Number
		{
			if ( m_loops == 0 )
			{
				return Number.POSITIVE_INFINITY;
			}
			else
			{
				return m_loops * ( 1 - ( position / duration ) );
			}
		}
		
		public function get loops():uint
		{
			return m_loops;
		}
		
		public function set loops( newLoops:uint ):void
		{
			//the change in loops
			var dLoops:int = newLoops - m_loops;
			
			if ( -dLoops >= m_loopsRemaining )//reduced loops so much that the sound should no longer play
			{
				stop();
			}
			else
			{
				m_loops = newLoops;
				m_loopsRemaining += dLoops;
			}
		}
		
		//--------------------------------------
		//  PRIVATE METHODS
		//--------------------------------------
		
		/**
		 * Internal method used to actually create the sound channel
		 * 
		 * @param	startTime
		 */
		protected function _play( startTime:Number = 0 ):void
		{
			trace( 'SoundNode::_play('+arguments+')' );
			if ( m_channel )//if the sound is currently playing
			{
				removeChannel();
			}
			
			m_channel = m_sound.play( startTime, 1, m_computedTransform );//play the sound
			
			m_channel.addEventListener( Event.SOUND_COMPLETE, loopComplete, false, 0, true );
		}
		
		/**
		 * determines what the playing status of this node should be
		 * 
		 * @param	isPaused - what the pause status of this sound currently is
		 */
		protected function _doPause( isPaused:Boolean ):void
		{
			var newPause:Boolean = _pause;//what the state of this sound should be right now
			
			if ( newPause != isPaused )
			{
				if ( newPause == true )
				{
					if ( m_sound )
					{
						if ( m_channel )
						{
							if ( m_isPlaying )
							{
								//record positions
								m_pausePosition = position % loopDuration;
								
								removeChannel();//stop playing sound
							}
						}
					}
				}
				else
				{
					if ( m_isPlaying )
					{
						if ( m_sound )
						{
							//play sound again
							_play( m_pausePosition );
						}
					}
				}
				
			}
		}
		
		/**
		 * Called whenever a change is made that requires an updated sound transform
		 */
		protected function updatedComputedTransform():void
		{
			if ( m_parentTransform )
			{
				//combine transforms
				m_computedTransform = addSoundTransforms( m_parentTransform, m_transform );
			}
			else
			{
				//no parent, just copy
				m_computedTransform = new SoundTransform( m_transform.volume, m_transform.pan );
				m_computedTransform.leftToLeft = m_transform.leftToLeft;
				m_computedTransform.leftToRight = m_transform.leftToRight;
				m_computedTransform.rightToLeft = m_transform.rightToLeft;
				m_computedTransform.rightToRight = m_transform.rightToRight;
			}
			
			if ( m_mute )//apply muting
			{
				m_computedTransform.volume = 0;
			}
			
			//tell children
			for ( var i:int = m_children.length - 1; i > -1; --i )
			{
				m_children[i].parentTransform = m_computedTransform;
			}
			
			if ( m_channel )
			{
				m_channel.soundTransform = m_computedTransform;
			}
			
			dispatchEvent( new SoundNodeEvent( SoundNodeEvent.TRANSFORM ) );
		}
		
		/**
		 * called each time the sound finishes looping
		 * 
		 * @param	evt
		 */
		protected function loopComplete( evt:Event ):void
		{
			trace( 'SoundNode::loopComplete('+arguments+')' );
			removeChannel();
			
			if ( m_loops == 0 )//set to infinitely loop
			{
				//just play again!
				_play();
			}
			else
			{
				if ( --m_loopsRemaining == 0 )//count down the remaining loops
				{
					dispatchEvent( new SoundNodeEvent( SoundNodeEvent.COMPLETE ) );
					
					m_endHandler();//played all the requested loops, call end handler (stop|clear|destroy)
				}
				else
				{
					//play the next loop
					_play();
				}
			}
		}
		
		/**
		 * used to remove the channel object
		 */
		protected function removeChannel():void
		{
			if ( m_channel )
			{
				m_channel.removeEventListener( Event.SOUND_COMPLETE, loopComplete );
				
				m_channel.stop();
				m_channel = null;
			}
		}
		
		//--------------------------------------
		//  PUBLIC STATIC METHODS
		//--------------------------------------
		
		/**
		 * Combines two sound transforms
		 * 
		 * @param	a - the first sound tranform
		 * @param	b - the second sound transform
		 * @return
		 */
		public static function addSoundTransforms( a:SoundTransform, b:SoundTransform ):SoundTransform
		{
			var t:SoundTransform = new SoundTransform();
			
			t.leftToLeft = a.leftToLeft * b.leftToLeft;			//average these values?
			t.leftToRight = a.leftToRight * b.leftToRight;		//
			t.rightToLeft = a.rightToLeft * b.rightToLeft;		//
			t.rightToRight = a.rightToRight * b.rightToRight;	//
			t.volume = a.volume * b.volume;
			t.pan = a.pan + b.pan;
			
			return t;
		}
		
		/**
		 * Factory method to simplify creating and playing a sound node
		 * 
		 * @param	snd
		 * @param	initVol
		 * @param	loops
		 * @return
		 */
		public static function createAndPlayNode( snd:*, initVol:Number = 1, loops:uint = 1 ):SoundNode
		{
			var sound:SoundNode = new SoundNode( getSndObj( snd ) );//get sound from snd arguments
			
			sound.volume = initVol;
			
			sound.play( 0, loops );
			
			return sound;
		}
		
		//--------------------------------------
		//  PRIVATE/PROTECTED STATIC METHODS
		//--------------------------------------
		
		/**
		 * Takes an argument and tries to get the approriate Sound object
		 * 
		 * @param	snd
		 * @return	A Sound object
		 */
		protected static function getSndObj( snd:* ):Sound
		{
			if ( snd is Array )
			{
				var sndArr:Array = snd as Array;
				
				//pick random elements
				return getSndObj( sndArr[Math.floor( Math.random() * sndArr.length )] );
			}
			
			if ( snd is Class )
			{
				try
				{
					var sound:Sound = ( new ( snd as Class )() as Sound );
					return sound;
				}
				
				catch ( e:Error )
				{
					throw new ArgumentError( 'Invalid argument for \'snd\'. If Class, must instanciate an object of type Sound' );
				}
			}
			else if ( snd is Sound )
			{
				return snd as Sound;
			}
			else if ( snd is String )
			{
				//try and load class, if that fails, assume it's a URL
				try
				{
					return getSndObj( getDefinitionByName( String( snd ) ) );
				}
				
				catch ( e:ReferenceError )
				{
					//class not found, assume it's actually meant to be a URL
					return new Sound( new URLRequest( String( snd ) ) );
				}
			}
			else
			{
				throw new ArgumentError( 'Invalid argument for \'snd\'. Must be of type Array, Class or String' );
			}
			
			return null;
		}
	}
}